﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.CommonUtilities.Reflectors;
using DomainDriver.DomainModeling.Attributes;
using DomainDriver.DomainModeling.DataProviders;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Queries;

namespace DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage
{
    public class EntityFrameworkDatabaseStorageManager<KEY, KEYED_DOMAIN_OBJECT, STORED_KEY, CONTEXT> : QueryableStorageManagerBase<KEY, KEYED_DOMAIN_OBJECT>
        where KEYED_DOMAIN_OBJECT : class, IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>, IEfEntity<STORED_KEY>
        where CONTEXT : DbContext
    {
        protected bool m_DomainObjectImplementsAggregateRootInterface = false;

        public EntityFrameworkDatabaseStorageManager(IDataProvider dataProvider)
            : base(dataProvider)
        {
            Type contextType = typeof(CONTEXT);
            if (!dataProvider.DataSourceType.IsSubclassOf(contextType) && !dataProvider.DataSourceType.Equals(contextType))
            {
                throw new ApplicationException();
            }

            Type domainObjectType = typeof(KEYED_DOMAIN_OBJECT);
            Type aggregateRootInterfaceType = typeof(IEfAggregate<KEYED_DOMAIN_OBJECT>);
            if (null != domainObjectType.GetInterface(aggregateRootInterfaceType.Name))
            { m_DomainObjectImplementsAggregateRootInterface = true; }
        }

        protected CONTEXT DataContext
        {
            get { return DataProvider.DataSource as CONTEXT; }
        }

        protected override IQueryable<KEYED_DOMAIN_OBJECT> GetQueryableDataStore()
        {
            DbSet<KEYED_DOMAIN_OBJECT> domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();
            return domainObjectSet;
        }

        protected override EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT> GetKeyMatchingPredicate(IEnumerable<KEY> keys)
        {
            var newDomainObject = this.CreateDefaultInstance_AsDbQueryable();
            var supports_UnCompiled = (newDomainObject != null);

            if (!supports_UnCompiled)
            {
                List<STORED_KEY> convertedKeys = new List<STORED_KEY>();
                foreach (KEY key in keys)
                {
                    STORED_KEY convertedKey = (STORED_KEY)Convert.ChangeType(key, typeof(STORED_KEY));
                    convertedKeys.Add(convertedKey);
                }

                Func<KEYED_DOMAIN_OBJECT, bool> keyMatchingPredicate = ((KEYED_DOMAIN_OBJECT domainObject) => convertedKeys.Contains(domainObject.EF_Id));
                return new EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT>(keyMatchingPredicate);
            }
            else
            {
                var keysAsSqlMatchableText = newDomainObject.GetKeysAsSqlMatchableText<KEY>(keys);
                var keyMatchingSql = newDomainObject.GetSqlCode_ForKeyMatching();

                var domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();
                var dbSqlQuery = domainObjectSet.CreateKeyMatchingDbSqlQuery<KEYED_DOMAIN_OBJECT>(keysAsSqlMatchableText, keyMatchingSql);
                return new EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT>(dbSqlQuery);
            }
        }

        protected override ICollection<KEY> ReadKeys(EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT> keyMatcher)
        {
            var domainObjects = PerformQuery(keyMatcher, false);
            var matchingKeys = new List<KEY>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                matchingKeys.Add(domainObject.Key);
            }
            return matchingKeys;
        }

        protected override ICollection<DESC> ReadDescriptors<DESC>(EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT> keyMatcher)
        {
            var domainObjects = PerformQuery(keyMatcher, false);
            var matchingDescriptors = new List<DESC>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                DESC descriptor = new DESC();
                descriptor.DescribedObject = domainObject;
                matchingDescriptors.Add(descriptor);
            }
            return matchingDescriptors;
        }

        protected override ICollection<KEYED_DOMAIN_OBJECT> ReadStoredObjects(EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT> keyMatcher)
        {
            var domainObjects = PerformQuery(keyMatcher, true);
            var matchingObjects = new List<KEYED_DOMAIN_OBJECT>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                matchingObjects.Add(domainObject);
            }
            return matchingObjects;
        }

        protected IEnumerable<KEYED_DOMAIN_OBJECT> PerformQuery(EntityKeyMatcher<KEY, KEYED_DOMAIN_OBJECT> keyMatcher, bool readAggregates)
        {
            var dataStore = GetQueryableDataStore();
            var domainObjects = keyMatcher.DoWhere(dataStore);

            if (!m_DomainObjectImplementsAggregateRootInterface || !readAggregates)
            { return domainObjects; }

            Dictionary<string, object> batchReadState = new Dictionary<string, object>();
            Type rootType = typeof(KEYED_DOMAIN_OBJECT);
            batchReadState.Add(rootType.Name, domainObjects);

            foreach (IEfAggregate<KEYED_DOMAIN_OBJECT> domainObject in domainObjects)
            {
                // LATER: Is there a way to use predicate instead of key matcher
                domainObject.ReadNestedAggregateValues(DataContext, keyMatcher, batchReadState);
            }
            return domainObjects;
        }

        protected override void WriteObjects(IEnumerable<KEYED_DOMAIN_OBJECT> domainObjects, ChangeActionType changeAction)
        {
            DbSet<KEYED_DOMAIN_OBJECT> domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                if (changeAction == ChangeActionType.Create)
                {
                    if (m_DomainObjectImplementsAggregateRootInterface)
                    {
                        (domainObject as IEfAggregate<KEYED_DOMAIN_OBJECT>).AddNestedAggregateValues(DataContext);
                    }
                    domainObjectSet.Add(domainObject);
                    continue;
                }

                List<KEY> domainObjectKeys = new List<KEY>();
                domainObjectKeys.Add(domainObject.Key);

                var keyMatcher = GetKeyMatchingPredicate(domainObjectKeys);
                var queryResults = PerformQuery(keyMatcher, true);

                KEYED_DOMAIN_OBJECT original = queryResults.First<KEYED_DOMAIN_OBJECT>();
                KEYED_DOMAIN_OBJECT copyOfOriginal = original.Copy();

                if (changeAction == ChangeActionType.Delete)
                {
                    domainObjectSet.Remove(original);
                    // NOTE: Need to delete nested objects before saving changes
                    if (m_DomainObjectImplementsAggregateRootInterface)
                    {
                        (copyOfOriginal as IEfAggregate<KEYED_DOMAIN_OBJECT>).DeleteNestedAggregateValues(DataContext);
                    }
                    continue;
                }

                // NOTE: Need to copy changes to domain object read directly from DbSet for change tracker to save changes
                KEYED_DOMAIN_OBJECT updatedOriginal = original;
                domainObject.CopyTo(updatedOriginal);
                if (m_DomainObjectImplementsAggregateRootInterface)
                {
                    (updatedOriginal as IEfAggregate<KEYED_DOMAIN_OBJECT>).UpdateNestedAggregateValues(DataContext, copyOfOriginal);
                }
            }

            if (!DataProvider.IsPerformingBatchChanges)
            { DataContext.SaveChanges(); }
        }
    }
}